package com.nervenets.general.jwt.util;

import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import com.alibaba.fastjson.JSON;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.nervenets.general.Global;
import com.nervenets.general.config.ApplicationProperties;
import com.nervenets.general.exception.TokenIllegalException;
import com.nervenets.general.jwt.aspect.JwtRole;
import com.nervenets.general.jwt.aspect.JwtSecurity;
import com.nervenets.general.model.MenuRole;
import com.nervenets.general.model.SecurityUser;
import com.nervenets.general.service.RedisService;
import com.nervenets.general.utils.HttpTools;
import com.nervenets.general.utils.JodaUtils;
import com.nervenets.general.utils.SpringContextHolder;
import com.nervenets.general.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.reflections.Reflections;
import org.reflections.scanners.SubTypesScanner;
import org.reflections.scanners.TypeAnnotationsScanner;
import org.reflections.util.ClasspathHelper;
import org.reflections.util.ConfigurationBuilder;
import org.reflections.util.FilterBuilder;
import org.springframework.util.DigestUtils;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.*;

import static com.nervenets.general.Global.TokenIllegalType.*;
import static com.nervenets.general.i18n.I18nTranslator.braceTranslate;
import static com.nervenets.general.i18n.I18nTranslator.translate;

@Slf4j
public final class JwtUtils {
    private static final String KEY = "JWT::SESSION::%s";

    public static String getToken(SecurityUser user, String license) {
        RedisService redisService = SpringContextHolder.getBean(RedisService.class);
        ApplicationProperties applicationProperties = SpringContextHolder.getBean(ApplicationProperties.class);
        String key = String.format(KEY, user.getId());
        if (applicationProperties.isSessionSingleSign()) {
            redisService.delete(key);
        }
        user.setExpireTime(JodaUtils.getTimestamp() + applicationProperties.getSessionTimeout() * 1000L);
        redisService.hset(key, license, JSON.toJSONString(user));
        return getToken(license, user.getId(), JodaUtils.parseDate(user.getExpireTime()));
    }

    public static void clearToken(long userId) {
        RedisService redisService = SpringContextHolder.getBean(RedisService.class);
        String key = String.format(KEY, userId);
        redisService.delete(key);
    }

    private static String getToken(String license, long userId, Date expiresAt) {
        return JWT.create()
                .withIssuer(license)
                .withIssuedAt(new Date())
                .withExpiresAt(expiresAt)
                .withAudience(String.valueOf(userId))
                .sign(Algorithm.HMAC512(license));
    }

    public static long getUserId(String token) {
        long userId;
        try {
            DecodedJWT jwt = JWT.decode(token);
            userId = Long.parseLong(jwt.getAudience().get(0));
        } catch (Exception e) {
            throw new TokenIllegalException(translate("application.login.expired"), VERIFY);
        }

        return userId;
    }

    public static void logout(String token, String license) {
        long userId;
        try {
            DecodedJWT jwt = JWT.decode(token);
            userId = Long.parseLong(jwt.getAudience().get(0));
        } catch (Exception ignored) {
            return;
        }
        RedisService redisService = SpringContextHolder.getBean(RedisService.class);

        String key = String.format(KEY, userId);
        redisService.hdel(key, license);
    }

    public static SecurityUser getUser() {
        try {
            return (SecurityUser) HttpTools.getHttpRequestAttribute(Global.Constants.SESSION_USER);
        } catch (Exception e) {
            return null;
        }
    }

    public static String getUserNameOrNull() {
        try {
            return Objects.requireNonNull(getUser()).getUsername();
        } catch (Exception e) {
            return null;
        }
    }

    public static SecurityUser getUser(String token, String license) {
        long userId;
        try {
            DecodedJWT jwt = JWT.decode(token);
            userId = Long.parseLong(jwt.getAudience().get(0));
        } catch (Exception e) {
            throw new TokenIllegalException(translate("application.login.expired"), VERIFY);
        }

        RedisService redisService = SpringContextHolder.getBean(RedisService.class);
        String key = String.format(KEY, userId);
        if (redisService.hexists(key, license)) {
            String value = redisService.hget(key, license);
            if (null == value) return null;

            SecurityUser user = JSON.parseObject(value, SecurityUser.class);
            if (null != user) {
                if (user.getExpireTime() < JodaUtils.getTimestamp()) {
                    redisService.hdel(key, license);
                    throw new TokenIllegalException(translate("application.login.expired") + "-timeout", EXPIRED);
                }
                ApplicationProperties applicationProperties = SpringContextHolder.getBean(ApplicationProperties.class);
                //当session过期时间少于了设置的一半，自动续期
                if (applicationProperties.isSessionAutoRenewal() && user.getExpireTime() - JodaUtils.getTimestamp() < applicationProperties.getSessionTimeout() / 2) {
                    user.setExpireTime(JodaUtils.getTimestamp() + applicationProperties.getSessionTimeout());
                    redisService.hset(key, license, JSON.toJSONString(user));
                }
            }
            return user;
        }
        return null;
    }

    /**
     * 获取所有注解的权限
     *
     * @return 权限组
     */
    public static List<MenuRole> getAllMenuRoles() {
        log.info("扫描权限树...");
        ApplicationProperties applicationProperties = SpringContextHolder.getBean(ApplicationProperties.class);

        String[] roleScanPackages = applicationProperties.getRoleScanPackages();
        if (null == roleScanPackages || roleScanPackages.length == 0) {
            log.warn("未设置权限扫描路径,忽略扫描...");
            return new ArrayList<>();
        }

        ConfigurationBuilder config = new ConfigurationBuilder();
        FilterBuilder filterBuilder = new FilterBuilder();
        List<URL> urls = new ArrayList<>();
        for (String roleScanPackage : roleScanPackages) {
            filterBuilder.includePackage(roleScanPackage);
            urls.addAll(ClasspathHelper.forPackage(roleScanPackage));
        }
        config.filterInputsBy(filterBuilder);
        config.addUrls(urls);
        config.setScanners(new TypeAnnotationsScanner(), new SubTypesScanner());
        Reflections f = new Reflections(config);

        Set<Class<?>> classSet = f.getTypesAnnotatedWith(JwtRole.class);

        List<String> actionIds = new ArrayList<>();
        List<MenuRole> menuRoles = new ArrayList<>();

        for (Class<?> c : classSet) {
            if (c.isAnnotationPresent(JwtRole.class)) {
                JwtRole role = c.getAnnotation(JwtRole.class);

                String group = role.group();
                String groupName = braceTranslate(role.groupName());
                String groupId = DigestUtils.md5DigestAsHex(group.getBytes());

                String function = role.function();
                String functionName = braceTranslate(role.functionName());
                String functionId = DigestUtils.md5DigestAsHex((group + function).getBytes());

                MenuRole groupMenuRole = new MenuRole(groupId, groupName, group, role.tags(), group, null, group, role.queue());
                MenuRole functionMenuRole = new MenuRole(functionId, functionName, function, role.tags(), org.apache.commons.lang3.StringUtils.joinWith("/", group, function),
                        role.component(), String.format("%s:%s", group, function), role.queue());

                if (!menuRoles.contains(groupMenuRole)) menuRoles.add(groupMenuRole);
                groupMenuRole = menuRoles.get(menuRoles.indexOf(groupMenuRole));

                if (!groupMenuRole.contains(functionMenuRole)) groupMenuRole.add(functionMenuRole);

                //log.info("{} -> {}", groupName, functionName);
                if (applicationProperties.isOpenFunctionRole()) {
                    Method[] methods = c.getMethods();
                    for (Method method : methods) {
                        if (method.isAnnotationPresent(JwtSecurity.class)) {
                            JwtSecurity jwtSecurity = method.getAnnotation(JwtSecurity.class);

                            if (jwtSecurity.required() && !StringUtils.isBlank(jwtSecurity.permission())) {
                                String permission = String.format("%s:%s:%s", group, function, jwtSecurity.permission());
                                String actionId = DigestUtils.md5DigestAsHex(permission.getBytes());
                                MenuRole action = new MenuRole(actionId, braceTranslate(jwtSecurity.permissionName()), jwtSecurity.permission(), jwtSecurity.tags(), jwtSecurity.router(), jwtSecurity.component(), permission, jwtSecurity.queue());
                                if (!actionIds.contains(actionId)) {
                                    functionMenuRole.add(action);
                                    actionIds.add(actionId);
                                }
                                //log.info("==> {}, permission: {}", jwtSecurity.permissionName(), permission);
                            }

                        }
                    }
                }
            }
        }
        menuRoles.sort(Comparator.comparingInt(MenuRole::getQueue));
        return menuRoles;
    }

    public static String license(HttpServletRequest request) {
        ApplicationProperties applicationProperties = SpringContextHolder.getBean(ApplicationProperties.class);
        return license(request, applicationProperties.isTokenValidateDevice(), applicationProperties.isTokenValidateIp());
    }

    public static String license(HttpServletRequest request, boolean validateDevice, boolean validateIp) {
        final UserAgent agent = UserAgentUtil.parse(request.getHeader("User-Agent"));
        String builder = request.getHeader(Global.Constants.PLATFORM_KEY) +
                (validateDevice ? String.format("%s%s,%s%s,%s%s",
                        agent.getBrowser(), agent.getVersion(),
                        agent.getPlatform(), agent.getOsVersion(),
                        agent.getEngine(), agent.getEngineVersion()) : "") +
                (validateIp ? HttpTools.getIpAddress(request) : "");
        return DigestUtils.md5DigestAsHex(builder.getBytes());
    }

    public static boolean verify(String token, String license) {
        JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC512(license)).build();
        try {
            jwtVerifier.verify(token);
            return true;
        } catch (JWTVerificationException e) {
            e.printStackTrace();
        }
        return false;
    }

    public static void authorize(String token, String license, Set<String> menus, Set<String> permissions) {
        long userId;
        try {
            DecodedJWT jwt = JWT.decode(token);
            userId = Long.parseLong(jwt.getAudience().get(0));
        } catch (Exception e) {
            throw new TokenIllegalException(translate("application.login.expired"), VERIFY);
        }

        RedisService redisService = SpringContextHolder.getBean(RedisService.class);
        String key = String.format(KEY, userId);
        if (redisService.hexists(key, license)) {
            String value = redisService.hget(key, license);
            if (null == value) throw new TokenIllegalException(translate("application.login.expired"), ERROR);

            SecurityUser user = JSON.parseObject(value, SecurityUser.class);
            if (null != user) {
                if (user.getExpireTime() < JodaUtils.getTimestamp()) {
                    redisService.hdel(key, license);
                    throw new TokenIllegalException(translate("application.login.expired") + "-timeout", EXPIRED);
                }
                ApplicationProperties applicationProperties = SpringContextHolder.getBean(ApplicationProperties.class);
                //当session过期时间少于了设置的一半，自动续期
                if (user.getExpireTime() - JodaUtils.getTimestamp() < applicationProperties.getSessionTimeout() / 2) {
                    user.setExpireTime(JodaUtils.getTimestamp() + applicationProperties.getSessionTimeout());
                }
                user.setMenus(menus);
                user.setPermissions(permissions);
                redisService.hset(key, license, JSON.toJSONString(user));
            }
        }
    }
}
